// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

library utf8_test;

import "package:expect/expect.dart";
import 'dart:convert';

String decode(List<int> bytes, int chunkSize) {
  StringBuffer buffer = new StringBuffer();
  var stringSink = new StringConversionSink.fromStringSink(buffer);
  var byteSink = new Utf8Decoder().startChunkedConversion(stringSink);
  int i = 0;
  while (i < bytes.length) {
    var nextChunk = <int>[];
    for (int j = 0; j < chunkSize; j++) {
      if (i < bytes.length) {
        nextChunk.add(bytes[i]);
        i++;
      }
    }
    byteSink.add(nextChunk);
  }
  byteSink.close();
  return buffer.toString();
}

String decodeAllowMalformed(List<int> bytes, int chunkSize) {
  StringBuffer buffer = new StringBuffer();
  var stringSink = new StringConversionSink.fromStringSink(buffer);
  var decoder = new Utf8Decoder(allowMalformed: true);
  var byteSink = decoder.startChunkedConversion(stringSink);
  int i = 0;
  while (i < bytes.length) {
    var nextChunk = <int>[];
    for (int j = 0; j < chunkSize; j++) {
      if (i < bytes.length) {
        nextChunk.add(bytes[i]);
        i++;
      }
    }
    byteSink.add(nextChunk);
  }
  byteSink.close();
  return buffer.toString();
}

final TESTS0 = [
  // Unfinished UTF-8 sequences.
  [0xc3],
  [0xE2, 0x82],
  [0xF0, 0xA4, 0xAD]
];

final TESTS1 = [
  // Overlong encoding of euro-sign.
  [0xF0, 0x82, 0x82, 0xAC],
  // Other overlong/unfinished sequences.
  [0xC0],
  [0xC1],
  [0xF5],
  [0xF6],
  [0xF7],
  [0xF8],
  [0xF9],
  [0xFA],
  [0xFB],
  [0xFC],
  [0xFD],
  [0xFE],
  [0xFF],
  [0xC0, 0x80],
  [0xC1, 0x80],
  // Outside valid range.
  [0xF4, 0xBF, 0xBF, 0xBF]
];

final TESTS2 = [
  // Test that 0xC0|1, 0x80 does not eat the next character.
  [
    [0xC0, 0x80, 0x61],
    "XXa"
  ],
  [
    [0xC1, 0x80, 0x61],
    "XXa"
  ],
  // 0xF5 .. 0xFF never appear in valid UTF-8 sequences.
  [
    [0xF5, 0x80],
    "XX"
  ],
  [
    [0xF6, 0x80],
    "XX"
  ],
  [
    [0xF7, 0x80],
    "XX"
  ],
  [
    [0xF8, 0x80],
    "XX"
  ],
  [
    [0xF9, 0x80],
    "XX"
  ],
  [
    [0xFA, 0x80],
    "XX"
  ],
  [
    [0xFB, 0x80],
    "XX"
  ],
  [
    [0xFC, 0x80],
    "XX"
  ],
  [
    [0xFD, 0x80],
    "XX"
  ],
  [
    [0xFE, 0x80],
    "XX"
  ],
  [
    [0xFF, 0x80],
    "XX"
  ],
  [
    [0xF5, 0x80, 0x61],
    "XXa"
  ],
  [
    [0xF6, 0x80, 0x61],
    "XXa"
  ],
  [
    [0xF7, 0x80, 0x61],
    "XXa"
  ],
  [
    [0xF8, 0x80, 0x61],
    "XXa"
  ],
  [
    [0xF9, 0x80, 0x61],
    "XXa"
  ],
  [
    [0xFA, 0x80, 0x61],
    "XXa"
  ],
  [
    [0xFB, 0x80, 0x61],
    "XXa"
  ],
  [
    [0xFC, 0x80, 0x61],
    "XXa"
  ],
  [
    [0xFD, 0x80, 0x61],
    "XXa"
  ],
  [
    [0xFE, 0x80, 0x61],
    "XXa"
  ],
  [
    [0xFF, 0x80, 0x61],
    "XXa"
  ],
  // Characters outside the valid range.
  [
    [0xF5, 0x80, 0x80, 0x61],
    "XXXa"
  ],
  [
    [0xF6, 0x80, 0x80, 0x61],
    "XXXa"
  ],
  [
    [0xF7, 0x80, 0x80, 0x61],
    "XXXa"
  ],
  [
    [0xF8, 0x80, 0x80, 0x61],
    "XXXa"
  ],
  [
    [0xF9, 0x80, 0x80, 0x61],
    "XXXa"
  ],
  [
    [0xFA, 0x80, 0x80, 0x61],
    "XXXa"
  ],
  [
    [0xFB, 0x80, 0x80, 0x61],
    "XXXa"
  ],
  [
    [0xFC, 0x80, 0x80, 0x61],
    "XXXa"
  ],
  [
    [0xFD, 0x80, 0x80, 0x61],
    "XXXa"
  ],
  [
    [0xFE, 0x80, 0x80, 0x61],
    "XXXa"
  ],
  [
    [0xFF, 0x80, 0x80, 0x61],
    "XXXa"
  ]
];

main() {
  var allTests = [...TESTS0, ...TESTS1].expand((test) {
    // Pairs of test and expected string output when malformed strings are
    // allowed. Replacement character: U+FFFD, one per unfinished sequence or
    // undecodable byte.
    String replacement =
        TESTS0.contains(test) ? "\u{FFFD}" : "\u{FFFD}" * test.length;
    return [
      [test, "${replacement}"],
      [
        [0x61, ...test],
        "a${replacement}"
      ],
      [
        [0x61, ...test, 0x61],
        "a${replacement}a"
      ],
      [
        [...test, 0x61],
        "${replacement}a"
      ],
      [
        [...test, ...test],
        "${replacement}${replacement}"
      ],
      [
        [...test, 0x61, ...test],
        "${replacement}a${replacement}"
      ],
      [
        [0xc3, 0xa5, ...test],
        "å${replacement}"
      ],
      [
        [0xc3, 0xa5, ...test, 0xc3, 0xa5],
        "å${replacement}å"
      ],
      [
        [...test, 0xc3, 0xa5],
        "${replacement}å"
      ],
      [
        [...test, 0xc3, 0xa5, ...test],
        "${replacement}å${replacement}"
      ]
    ];
  });

  var allTests2 = TESTS2.map((test) {
    // Pairs of test and expected string output when malformed strings are
    // allowed. Replacement character: U+FFFD
    String expected = (test[1] as String).replaceAll("X", "\u{FFFD}");
    return [test[0], expected];
  });

  for (var test in []..addAll(allTests)..addAll(allTests2)) {
    List<int> bytes = test[0];
    Expect.throwsFormatException(() => decode(bytes, 1));
    Expect.throwsFormatException(() => decode(bytes, 2));
    Expect.throwsFormatException(() => decode(bytes, 3));
    Expect.throwsFormatException(() => decode(bytes, 4));

    String expected = test[1];
    Expect.equals(expected, decodeAllowMalformed(bytes, 1));
    Expect.equals(expected, decodeAllowMalformed(bytes, 2));
    Expect.equals(expected, decodeAllowMalformed(bytes, 3));
    Expect.equals(expected, decodeAllowMalformed(bytes, 4));
  }
}
